/**
 * Created with IntelliJ IDEA.
 * User: ajhin
 * Date: 4/12/14
 * Time: 5:58 PM
 * To change this template use File | Settings | File Templates.
 */
package societyGames.codeAlong.model.errorChecking
{
import com.societyGames.codeAlong.codeExercise.Compiler;
import com.societyGames.codeAlong.state.codeExercise.model.CaseData;
import com.societyGames.codeAlong.state.codeExercise.model.ExceptionEvent;
import com.societyGames.codeAlong.state.codeExercise.model.testCase.TestCaseController;
import com.societyGames.system.EventObject;
import com.societyGames.system.FileCache;
import com.societyGames.utility.XMLDictionary;

import flash.events.Event;
import flash.events.EventDispatcher;
import flash.utils.ByteArray;

public class SolutionChecker extends EventDispatcher
{
  private static const EXPECTED_EXCEPTION_MESSAGE:String = "_SolutionChecker Generated Exception_";
  private var _completedEventObject:EventObject;
  private var _currentModuleIndexChangedEventObject:EventObject;
  private var _fileCache:FileCache = new FileCache();
  private var _xmlDictionary:XMLDictionary;
  private var _moduleXMLList:XMLList;
  private var _currentModuleIndex:int = 0;
  private var _lastModuleIndex:int;
  private var _solutionXMLList:XMLList;
  private var _testCaseController:TestCaseController;
  private var _errors:Vector.<String> = new Vector.<String>();
  private var _generateCode:Function; //(moduleXML:XML):String

  public function get errors():Vector.<String>
  {
    return _errors;
  }

  public function get completedEventObject():EventObject
  {
    return this._completedEventObject;
  }

  public function get currentModuleIndexChangedEventObject():EventObject
  {
    return this._currentModuleIndexChangedEventObject;
  }

  public function SolutionChecker()
  {
    _completedEventObject = new EventObject(this, "COMPLETED", Event);
    _currentModuleIndexChangedEventObject = new EventObject(this, "MODULE_INDEX_CHANGED", Event);
  }

  public function testSolutions(modulesXML:XML, solutionsXML:XML, generateCode:Function,
                                startIndex:int = 0, endIndex:int = -1):void
  {
    _errors.length = 0;
    _moduleXMLList = modulesXML.module;
    _currentModuleIndex = startIndex;
    if (endIndex < 0 || endIndex >= _moduleXMLList.length())
    {
      _lastModuleIndex = _moduleXMLList.length() - 1;
    }
    else
    {
      _lastModuleIndex = endIndex;
    }
    _solutionXMLList = solutionsXML.solution;
    _xmlDictionary = new XMLDictionary(modulesXML.entry);
    _generateCode = generateCode;

    Compiler.instance.initializedEventObject.addEventListener(onCompilerInitialized, false, 0, true);
    Compiler.instance.initialize();
  }

  public function generateSyntaxError(moduleXML:XML):String
  {
    return ".>.";
  }

  public function generateException(moduleXML:XML):String
  {
    var code:String = findSolutionCode(moduleXML);
    if (code == null)
    {
      code = "";
    }
    code += "\nthrow new Error(\"" + EXPECTED_EXCEPTION_MESSAGE + "\")";
    return code;
  }

  public function generateFail(moduleXML:XML):String
  {
    return "";
  }

  private function findSolutionCode(moduleXML:XML):String
  {
    var code:String;
    //Look for the solution associated with this module.
    for each (var solutionXML:XML in this._solutionXMLList)
    {
      if (solutionXML.@id == moduleXML.@id)
      {
        code = solutionXML.code[0];
        break;
      }
    }
    return code;
  }

  private function addCodeBeginCodeEnd(moduleXML:XML, code:String):String
  {
    if (moduleXML.hasOwnProperty("codeBegin"))
    {
      code = _xmlDictionary.lookUp(moduleXML.codeBegin[0]).toString() + "\n" + code;
    }
    if (moduleXML.hasOwnProperty("codeEnd"))
    {
      code += "\n" + _xmlDictionary.lookUp(moduleXML.codeEnd[0]).toString();
    }
    return code;
  }

  public function generateSolutionCode(moduleXML:XML):String
  {
    var code:String = findSolutionCode(moduleXML);

    //We didn't find a solution.
    if (code == null || code.length == 0)
    {
      throw new Error("Missing solution.");
    }

    return code;
  }

  private function onCompilerInitialized(event:Event):void
  {
    Compiler.instance.initializedEventObject.removeEventListener(onCompilerInitialized);
    testCurrentModule();
  }

  private function testCurrentModule():void
  {
    if (_currentModuleIndex > _lastModuleIndex)
    {
      _completedEventObject.dispatchEvent();
      return;
    }

    //If we're done...
    _currentModuleIndexChangedEventObject.dispatchEvent();

    var moduleXML:XML = _moduleXMLList[_currentModuleIndex];

    //If it can't complete, don't try to even find a solution.
    if (moduleXML.@canComplete == "false")
    {
      onSolutionError("Cannot complete.");
      return;
    }

    try
    {
      var code:String = _generateCode(moduleXML);
    }
    catch (error:Error)
    {
      onSolutionError(error.message);
      return;
    }

    code = addCodeBeginCodeEnd(moduleXML, code);

    //Generate the cases and executeByteArray for TestCaseManager
    var caseDatas:Vector.<CaseData> = new Vector.<CaseData>();
    for each (var caseXML:XML in moduleXML["case"])
    {
      caseDatas.push(CaseData.fromXml(caseXML));
    }

    try
    {
      var executeByteArray:ByteArray = Compiler.instance.compileSwf(code, "Test");
    }
    catch (error:Error)
    {
      if (_generateCode == generateSyntaxError)
      {
        onSolutionSuccess("Expected syntax error occurred.");
      }
      else
      {
        onSolutionError("Syntax Error: " + error.message);
      }
      return;
    }
    _testCaseController = new TestCaseController(_fileCache, caseDatas);
    _testCaseController.testCasesFailedEventObject.addEventListener(onAllTestCasesFailed, false, 0, true);
    _testCaseController.testCasesSucceededEventObject.addEventListener(onAllTestCasesSucceeded, false, 0, true);
    _testCaseController.unhandledExceptionEventObject.addEventListener(onUnhandledException);
    _testCaseController.exceptionEventObject.addEventListener(onException);

    _testCaseController.selectCase(0);
    _testCaseController.execute(executeByteArray);
  }

  private function cleanUpTestCaseController():void
  {
    _testCaseController.testCasesFailedEventObject.removeEventListener(onAllTestCasesFailed);
    _testCaseController.testCasesSucceededEventObject.removeEventListener(onAllTestCasesSucceeded);
    _testCaseController.unhandledExceptionEventObject.removeEventListener(onUnhandledException);
    _testCaseController.exceptionEventObject.removeEventListener(onException);
  }

  private function onSolutionError(description:String):void
  {
    var moduleXML:XML = _moduleXMLList[currentModuleIndex];
    var error:String = "Error: " + moduleXML.@id + ":" + moduleXML.@name + ": " + description;
    trace(error);
    _errors.push(error);
    _currentModuleIndex++;
    testCurrentModule();
  }

  private function onSolutionSuccess(description:String):void
  {
    var moduleXML:XML = _moduleXMLList[_currentModuleIndex];
    var success:String = "Success: " + moduleXML.@id + ":" + moduleXML.@name + ": "+ description;
    trace(success);
    _currentModuleIndex++;
    testCurrentModule();
  }

  private function onAllTestCasesFailed(event:Event):void
  {
    cleanUpTestCaseController();
    if (_generateCode == generateFail)
    {
      onSolutionSuccess("Expected fail occurred.");
    }
    else
    {
      onSolutionError("Unexpected goal failure.");
    }
  }

  private function onAllTestCasesSucceeded(event:Event):void
  {
    cleanUpTestCaseController();
    if (_generateCode == generateSolutionCode)
    {
      onSolutionSuccess("Expected success occurred.");
    }
    else
    {
      onSolutionError("Unexpected goal success.");
    }
  }

  private function onUnhandledException(event:Event):void
  {
    cleanUpTestCaseController();
    onSolutionError("Unhandled exception");
  }

  private function onException(event:ExceptionEvent):void
  {
    cleanUpTestCaseController();
    if (_generateCode == generateException && event.exception.error.message == EXPECTED_EXCEPTION_MESSAGE)
    {
      onSolutionSuccess("Expected exception occurred.");
    }
    else
    {
      onSolutionError("Unexpected exception: " +  event.exception.error.message);
    }
  }

  public function get currentModuleIndex():int
  {
    return _currentModuleIndex;
  }

  public function get currentModuleId():String
  {
    var moduleXML:XML = _moduleXMLList[_currentModuleIndex];
    return moduleXML.@id;
  }
}
}
